import logging
import os
import time
import traceback
import struct
from datetime import datetime

from configs import config
from handlers.connection import connection
from utils.decorator import async_func
from utils.enums import CommandEnum

logger = logging.getLogger(__name__)


class MsgTool:

    @staticmethod
    def one2two_byte(num):
        return divmod(num, 256)

    def crc(self, msg):
        code = self._do_crc16(msg)
        nums = self.one2two_byte(code)
        msg.extend(nums)

    @staticmethod
    def _do_crc16(array):
        """
        计算 crc16 XMODEM
        :param array:
        :return:
        """
        code = 0x0000
        for i in range(len(array)):
            code = ((array[i] << 8) ^ code) & 0xffff
            for j in range(0, 8):
                if code & 0x8000:
                    code = (code << 1) ^ 0x1021
                else:
                    code <<= 1
                code &= 0xffff
        return code

    def generate(self, *, command, token, length=0, content=None):
        msg = [config.FRAME_HEAD, token, command]
        if length:
            msg.extend(self.one2two_byte(length))
        else:
            if content:
                msg.extend(self.one2two_byte(len(content)))
            else:
                msg.extend(self.one2two_byte(length))
        if content:
            msg.extend(content)
        self.crc(msg)
        return msg

    def _re_hex(self, data, is_len=False, is_content=False, command=None, is_code=False):
        try:
            if is_len:
                return (data[0] << 8) + data[-1]
            elif is_content:
                return self._parse_content(command, data)
            elif is_code:
                return data.hex()
            return data
        except Exception as e:
            logger.error(f're-hex {data} error, msg: {e.args}')
            return

    @staticmethod
    def _parse_content(command, msg):
        if command == CommandEnum.CREATE:
            status = msg[0]
            bv = msg[1]
            sv = msg[2]
            update_time = msg[3: 7].hex()
            sn = msg[7:-1]
            return dict(status=status, bv=bv, sv=sv, update_time=update_time, sn=sn)
        elif command == CommandEnum.UPLOAD:
            bx, by, bz, bmx, bmy, bmz, bhc, bc, bmc, lp_bx, lp_by, lp_bz, lp_bmx, lp_bmy, lp_bmz, lp_bhc, lp_bc, lp_bmc = struct.unpack('!18f', msg)
            return dict(
                bx=bx, by=by, bz=bz, bmx=bmx, bmy=bmy, bmz=bmz, bhc=bhc, bc=bc, bmc=bmc, lp_bx=lp_bx, lp_by=lp_by,
                lp_bz=lp_bz, lp_bmx=lp_bmx, lp_bmy=lp_bmy, lp_bmz=lp_bmz, lp_bhc=lp_bhc, lp_bc=lp_bc, lp_bmc=lp_bmc
            )
        else:
            return [i for i in msg]

    @staticmethod
    def get_sn(data):
        return data['content']['sn'].decode()

    def _parse(self, msg, need_content=True):
        if not msg:
            return
        try:
            header = self._re_hex(msg[0])
            token = self._re_hex(msg[1])
            command = self._re_hex(msg[2])
            length = self._re_hex(msg[3:5], is_len=True)
            if not need_content:
                return dict(header=header, token=token, command=command, length=length)
            if length:
                content = self._re_hex(msg[5: 5 + length], command=command, is_content=True)
            else:
                content = None
            code = self._re_hex(msg[-2:], is_code=True)
            logger.info(
                f'header: {header}, token: {token}, command: {command}, length: {length}, code: {code}, content: {content}')
            result = dict(header=header, token=token, command=command, length=length, content=content, code=code)
            return result
        except Exception:
            logger.error(f"process msg error, msg: {traceback.format_exc()}")
            return

    def receive(self, handler, msg, need_content=True):
        if not msg:
            return
        if need_content:
            logger.info(f'get {handler} request, msg: {msg}')
        data = self._parse(msg, need_content=need_content)
        return data

    def get_assigned_content_msg(self, *, handler, command, length):
        msg = handler.request.recv(length + 2).strip()
        # logger.info(f'get assigned length msg: {msg}, len: {len(msg)}')
        if not msg or length == 0:
            return None, None
        code = self._re_hex(msg[-2:], is_code=True)
        content = self._parse_content(command, msg[:-2])
        return content, code

    def send(self, handler, data: bytes):
        handler.request.sendall(data)
        logger.info(f'msg: {data} send to client: {handler} success')


class InstructionHandler:
    """
    指令处理类
    """

    def start(self, handler, data):
        if not data:
            return
        command = data['command']
        try:
            if command == CommandEnum.CREATE:
                connection.add(handler=handler, data=data)
                self._receive_msg(handler)
                self._keep_alive(handler, data)
            elif command == CommandEnum.WRITE:
                self._write(handler, data)
            elif command == CommandEnum.READ:
                self._read(handler, data)
            elif command == CommandEnum.UPLOAD:
                handler.upload_data = None
                self._upload(handler, data)
        except Exception:
            logger.error(f'process command: {command} error, msg: {traceback.format_exc()}')
            return

    # @async_func
    def _keep_alive(self, handler, data):
        msg = msg_tool.generate(command=CommandEnum.CREATE, token=data['token'])
        sn = msg_tool.get_sn(data)
        error_nums = 0
        while True:
            token = connection.get_token(handler)
            try:
                msg_tool.send(handler, bytes(msg))
                msg = msg_tool.generate(command=CommandEnum.KEEP, token=token)
                time.sleep(config.LOOP_SLEEP_TIME)
            except OSError as e:
                if error_nums > 5:
                    logger.error(f'keep alive error, msg: {e.args}')
                    connection.delete(sn=sn)
                    break
                else:
                    error_nums += 1
            except Exception:
                logger.error(f'keep alive error, msg: {traceback.format_exc()}')
                break

    @async_func
    def _receive_msg(self, handler):
        index = 1
        while True:
            try:
                data = msg_tool.receive(handler, handler.request.recv(5).strip(), need_content=False)
                if not data:
                    continue

                data['content'], data['code'] = msg_tool.get_assigned_content_msg(
                    handler=handler, command=data['command'], length=data['length']
                )
                if data['command'] != CommandEnum.UPLOAD:
                    continue
                if index % 100 == 0:
                    logger.info(f'get upload msg, processed result: {data}')
                    index = 1

                self._upload(handler, data)
                index += 1
            except OSError as e:
                logger.error(f'connection error occurred when try to get client msg, msg: {e.args}')
                connection.delete(sn=handler.sn)
                break
            except Exception:
                logger.error(f'process client msg error, msg: {traceback.format_exc()}, will continue')
                continue
                # connection.delete(sn=handler.sn)
                # break

    def _write(self, handler, data):
        handler.data = data

    # @async_func
    def _upload(self, handler, data):
        handler.upload_data = data['content']
        upload_file = f'{config.BASE_DIR}{os.sep}files{os.sep}{handler.sn}-{datetime.now().date()}.csv'
        body = [handler.hz, handler.m]
        body.extend(list(data['content'].values()))

        if not os.path.exists(upload_file):
            with open(upload_file, 'w', newline='') as f:
                self._write_file(f, body, header=config.CSV_HEADERS)
        else:
            with open(upload_file, 'a+', newline='') as f:
                self._write_file(f, body)

    def _read(self, handler, data):
        pass

    @staticmethod
    def _write_file(file_no, body, header=None):
        import csv
        spamwriter = csv.writer(file_no, quoting=csv.QUOTE_MINIMAL)
        if header:
            spamwriter.writerow(header)
        spamwriter.writerow(body)


msg_tool = MsgTool()
command_tool = InstructionHandler()
